/**
 * menu-aim is a jQuery plugin for dropdown menus that can differentiate
 * between a user trying hover over a dropdown item vs trying to navigate into
 * a submenu's contents.
 *
 * menu-aim assumes that you have are using a menu with submenus that expand
 * to the menu's right. It will fire events when the user's mouse enters a new
 * dropdown item *and* when that item is being intentionally hovered over.
 *
 * __________________________
 * | Monkeys  >|   Gorilla  |
 * | Gorillas >|   Content  |
 * | Chimps   >|   Here     |
 * |___________|____________|
 *
 * In the above example, "Gorillas" is selected and its submenu content is
 * being shown on the right. Imagine that the user's cursor is hovering over
 * "Gorillas." When they move their mouse into the "Gorilla Content" area, they
 * may briefly hover over "Chimps." This shouldn't close the "Gorilla Content"
 * area.
 *
 * This problem is normally solved using timeouts and delays. menu-aim tries to
 * solve this by detecting the direction of the user's mouse movement. This can
 * make for quicker transitions when navigating up and down the menu. The
 * experience is hopefully similar to amazon.com/'s "Shop by Department"
 * dropdown.
 *
 * Use like so:
 *
 *      $("#menu").menuAim({
 *          activate: $.noop,  // fired on row activation
 *          deactivate: $.noop,  // fired on row deactivation
 *      });
 *
 *  ...to receive events when a menu's row has been purposefully (de)activated.
 *
 * The following options can be passed to menuAim. All functions execute with
 * the relevant row's HTML element as the execution context ('this'):
 *
 *      .menuAim({
 *          // Function to call when a row is purposefully activated. Use this
 *          // to show a submenu's content for the activated row.
 *          activate: function() {},
 *
 *          // Function to call when a row is deactivated.
 *          deactivate: function() {},
 *
 *          // Function to call when mouse enters a menu row. Entering a row
 *          // does not mean the row has been activated, as the user may be
 *          // mousing over to a submenu.
 *          enter: function() {},
 *
 *          // Function to call when mouse exits a menu row.
 *          exit: function() {},
 *
 *          // Selector for identifying which elements in the menu are rows
 *          // that can trigger the above events. Defaults to "> li".
 *          rowSelector: "> li",
 *
 *          // You may have some menu rows that aren't submenus and therefore
 *          // shouldn't ever need to "activate." If so, filter submenu rows w/
 *          // this selector. Defaults to "*" (all elements).
 *          submenuSelector: "*"
 *      });
 *
 * https://github.com/kamens/jQuery-menu-aim
*/
(function($) {
    $.fn.menuAim = function(opts) {

        var $menu = $(this),
            activeRow = null,
            mouseLocs = [],
            lastDelayLoc = null,
            timeoutId = null,
            options = $.extend({
                rowSelector: "> li",
                submenuSelector: "*",
                tolerance: 75,  // bigger = more forgivey when entering submenu
                enter: $.noop,
                exit: $.noop,
                activate: $.noop,
                deactivate: $.noop
            }, opts);

        var MOUSE_LOCS_TRACKED = 3,  // number of past mouse locations to track
            DELAY = 300;  // ms delay when user appears to be entering submenu

        /**
         * Keep track of the last few locations of the mouse.
         */
        var mousemoveDocument = function(e) {
                mouseLocs.push({x: e.pageX, y: e.pageY});

                if (mouseLocs.length > MOUSE_LOCS_TRACKED) {
                    mouseLocs.shift();
                }
            };

        /**
         * Cancel possible row activations when leaving the menu entirely
         */
        var mouseleaveMenu = function() {
            // codehint (sm-add) > bug
            if (activeRow) {
                options.deactivate(activeRow);
                activeRow = null;
            }
            // end codehint
            if (timeoutId) {
                    clearTimeout(timeoutId);
                }
            };

        /**
         * Trigger a possible row activation whenever entering a new row.
         */
        var mouseenterRow = function() {
            if (timeoutId) {
                // Cancel any previous activation delays
                clearTimeout(timeoutId);
            }

            options.enter(this);
            possiblyActivate(this);
        },
        mouseleaveRow = function() {
            options.exit(this);
        };

        /**
         * Activate a menu row.
         */
        var activate = function(row) {
            if (row == activeRow) {
                return;
            }

            if (activeRow) {
                options.deactivate(activeRow);
            }

            options.activate(row);
            activeRow = row;
        };

        /**
         * Possibly activate a menu row. If mouse movement indicates that we
         * shouldn't activate yet because user may be trying to enter
         * a submenu's content, then delay and check again later.
         */
        var possiblyActivate = function(row) {
                var delay = activationDelay();

                if (delay) {
                    timeoutId = setTimeout(function() {
                        possiblyActivate(row);
                    }, delay);
                } else {
                    activate(row);
                }
            };

        /**
         * Return the amount of time that should be used as a delay before the
         * currently hovered row is activated.
         *
         * Returns 0 if the activation should happen immediately. Otherwise,
         * returns the number of milliseconds that should be delayed before
         * checking again to see if the row should be activated.
         */
        var activationDelay = function() {

            // !!!!!!CODEHINT (SM-EDIT)!!!!!!! > massively edited
            // in order to implement right alignment.

            if (!activeRow || !$(activeRow).is(options.submenuSelector)) {
                // If there is no other submenu row already active, then
                // go ahead and activate immediately.
                return 0;
            }

            var dir = $menu.is(".pull-right") ? "left" : "right";

            var offset = $menu.offset(),
                upperBound = {
                    x: dir === "right" ? offset.left + $menu.outerWidth() : offset.left,
                    //x: offset.left + $menu.outerWidth(),
                    y: offset.top - options.tolerance
                },
                lowerBound = {
                    x: dir === "right" ? offset.left + $menu.outerWidth() : offset.left,
                    //x: offset.left + $menu.outerWidth(),
                    y: offset.top + $menu.outerHeight() + options.tolerance
                },
                loc = mouseLocs[mouseLocs.length - 1],
                prevLoc = mouseLocs[0];

            if (!loc) {
                return 0;
            }

            if (!prevLoc) {
                prevLoc = loc;
            }


            // If the previous mouse location was outside of the entire
            // menu's bounds, immediately activate.
            var wasOutsideOfBounds;
            if (dir == "right") {
                wasOutsideOfBounds = prevLoc.x < offset.left || prevLoc.x > lowerBound.x ||
                                     prevLoc.y < offset.top || prevLoc.y > lowerBound.y;
            }
            else {
                wasOutsideOfBounds = prevLoc.x > (offset.left + $menu.outerWidth()) || prevLoc.x < lowerBound.x ||
                                     prevLoc.y > (offset.top + $menu.outerHeight()) || prevLoc.y > lowerBound.y;
            }
            if (wasOutsideOfBounds) {
                return 0;
            }


            // If the mouse hasn't moved since the last time we checked
            // for activation status, immediately activate.
            if (lastDelayLoc && loc.x == lastDelayLoc.x && loc.y == lastDelayLoc.y) {
                return 0;
            }


            // Detect if the user is moving towards the currently activated
            // submenu.
            //
            // If the mouse is heading relatively clearly towards
            // the submenu's content, we should wait and give the user more
            // time before activating a new row. If the mouse is heading
            // elsewhere, we can immediately activate a new row.
            //
            // We detect this by calculating the slope formed between the
            // current mouse location and the upper/lower right points of
            // the menu. We do the same for the previous mouse location.
            // If the current mouse location's slopes are
            // increasing/decreasing appropriately compared to the
            // previous's, we know the user is moving toward the submenu.
            //
            // Note that since the y-axis increases as the cursor moves
            // down the screen, we are looking for the slope between the
            // cursor and the upper left corner to decrease over time, not
            // increase (somewhat counterintuitively).
            function slope(a, b) {
                return (b.y - a.y) / (b.x - a.x);
            };

            var upperSlope = slope(loc, upperBound),
                lowerSlope = slope(loc, lowerBound),
                prevUpperSlope = slope(prevLoc, upperBound),
                prevLowerSlope = slope(prevLoc, lowerBound);

            var isAiming;
            if (dir == "right") {
                isAiming = upperSlope < prevUpperSlope && lowerSlope > prevLowerSlope;
            }
            else {
                isAiming = upperSlope > prevUpperSlope && lowerSlope < prevLowerSlope;
            }
            if (isAiming) {
                // Mouse is moving from previous location towards the
                // currently activated submenu. Delay before activating a
                // new menu row, because user may be moving into submenu.
                lastDelayLoc = loc;
                return DELAY;
            }

            lastDelayLoc = null;
            return 0;
        };

        /**
         * Hook up initial menu events
         */
        var init = function() {
            $menu
                .mouseleave(mouseleaveMenu)
                .find(options.rowSelector)
                    .mouseenter(mouseenterRow)
                    .mouseleave(mouseleaveRow);

            $(document).mousemove(mousemoveDocument);
        };

        init();
        return this;
    };
})(jQuery);

